/*
 * Copyright (C) 2016 The Dagger Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dagger.internal.codegen.binding;

import static androidx.room.compiler.processing.XTypeKt.isArray;
import static dagger.internal.codegen.binding.SourceFiles.classFileName;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
import static dagger.internal.codegen.xprocessing.XAnnotationValues.characterLiteralWithSingleQuotes;
import static dagger.internal.codegen.xprocessing.XCodeBlocks.makeParametersCodeBlock;
import static dagger.internal.codegen.xprocessing.XCodeBlocks.toParametersCodeBlock;
import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
import static dagger.internal.codegen.xprocessing.XTypes.asArray;
import static dagger.internal.codegen.xprocessing.XTypes.isTypeOf;

import androidx.room.compiler.codegen.XClassName;
import androidx.room.compiler.codegen.XCodeBlock;
import androidx.room.compiler.processing.XAnnotation;
import androidx.room.compiler.processing.XAnnotationValue;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import dagger.internal.codegen.xprocessing.XTypeNames;

/**
 * Returns an expression creating an instance of the visited annotation type. Its parameter must be
 * a class as generated by {@link dagger.internal.codegen.writing.AnnotationCreatorGenerator}.
 *
 * <p>Note that {@code AnnotationValue#toString()} is the source-code representation of the value
 * <em>when used in an annotation</em>, which is not always the same as the representation needed
 * when creating the value in a method body.
 *
 * <p>For example, inside an annotation, a nested array of {@code int}s is simply {@code {1, 2, 3}},
 * but in code it would have to be {@code new int[] {1, 2, 3}}.
 */
public final class AnnotationExpression {
  private final XAnnotation annotation;
  private final XClassName creatorClass;

  AnnotationExpression(XAnnotation annotation) {
    this.annotation = annotation;
    this.creatorClass = getAnnotationCreatorClassName(annotation.getType().getTypeElement());
  }

  /**
   * Returns an expression that calls static methods on the annotation's creator class to create an
   * annotation instance equivalent the annotation passed to the constructor.
   */
  XCodeBlock getAnnotationInstanceExpression() {
    return getAnnotationInstanceExpression(annotation);
  }

  private XCodeBlock getAnnotationInstanceExpression(XAnnotation annotation) {
    return XCodeBlock.of(
        "%T.%L(%L)",
        creatorClass,
        createMethodName(annotation.getType().getTypeElement()),
        makeParametersCodeBlock(
            annotation.getAnnotationValues().stream()
                .map(this::getValueExpression)
                .collect(toImmutableList())));
  }

  /**
   * Returns the name of the generated class that contains the static {@code create} methods for an
   * annotation type.
   */
  public static XClassName getAnnotationCreatorClassName(XTypeElement annotationType) {
    XClassName annotationTypeName = annotationType.asClassName();
    return XClassName.Companion.get(
        annotationTypeName.getPackageName(),
        classFileName(annotationTypeName) + "Creator");
  }

  public static String createMethodName(XTypeElement annotationType) {
    return "create" + getSimpleName(annotationType);
  }

  /**
   * Returns an expression that evaluates to a {@code value} of a given type on an {@code
   * annotation}.
   */
  XCodeBlock getValueExpression(XAnnotationValue value) {
    if (isArray(value.getValueType())) {
      XType componentType = asArray(value.getValueType()).getComponentType();
      return XCodeBlock.of(
          "new %T[] {%L}",
          // TODO(b/264464791): The KClass -> Class swap can be removed once this bug is fixed.
          isTypeOf(componentType, XTypeNames.KCLASS)
              ? XTypeNames.CLASS
              : componentType.getRawType().asTypeName(),
          value.asAnnotationValueList().stream()
              .map(this::getValueExpression)
              .collect(toParametersCodeBlock()));
    } else if (value.hasEnumValue()) {
      return XCodeBlock.of(
          "%T.%L",
          value.asEnum().getEnclosingElement().asClassName(), getSimpleName(value.asEnum()));
    } else if (value.hasAnnotationValue()) {
      return getAnnotationInstanceExpression(value.asAnnotation());
    } else if (value.hasTypeValue()) {
      return XCodeBlock.of("%T.class", value.asType().getTypeElement().asClassName());
    } else if (value.hasStringValue()) {
      return XCodeBlock.of("%S", value.asString());
    } else if (value.hasByteValue()) {
      return XCodeBlock.of("(byte) %L", value.asByte());
    } else if (value.hasCharValue()) {
      // TODO(bcorso): Replace when https://github.com/square/javapoet/issues/698 is fixed.
      return XCodeBlock.of("%L", characterLiteralWithSingleQuotes(value.asChar()));
    } else if (value.hasDoubleValue()) {
      return XCodeBlock.of("%LD", value.asDouble());
    } else if (value.hasFloatValue()) {
      return XCodeBlock.of("%LF", value.asFloat());
    } else if (value.hasLongValue()) {
      return XCodeBlock.of("%LL", value.asLong());
    } else if (value.hasShortValue()) {
      return XCodeBlock.of("(short) %L", value.asShort());
    } else {
      return XCodeBlock.of("%L", value.getValue());
    }
  }
}
